import logging, pathlib, os, re, mimetypes, subprocess, shlex, shutil
from send2trash import send2trash as trashfile

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *

from protolib import convert_size

from protolib.gui_utils.pyqt5_messageboxes import show_critical_messagebox

from windows.to_protolib.text_input import ask_text

logger = logging.getLogger(__name__)

class BrowserTreeView(QTreeView):
    editor_closed = pyqtSignal()

    def __init__(self):
        super().__init__()

    def closeEditor(self, *args):
        logger.debug('editor_closed')
        self.editor_closed.emit()
        QTreeView.closeEditor(self, *args)

class FileBrowserWidget(QWidget):
    FILE_NAME, FILE_TYPE, FILE_SIZE, FILE_MIMETYPE = range(4)

    directory_listing_refreshed = pyqtSignal()

    def __init__(self):
        super().__init__()

        #region Widgets
        layout = QVBoxLayout()
        self.setLayout(layout)

        pathBarLayout = QHBoxLayout()
        self.pathedit = QLineEdit()
        self.pathedit.setPlaceholderText('Enter file path here')
        self.pathedit.editingFinished.connect(self.pathedit_text_updated)
        pathBarLayout.addWidget(self.pathedit)
        self.parentButton = QPushButton('Parent')
        self.parentButton.setIcon(self.style().standardIcon(QStyle.SP_FileDialogToParent))
        self.parentButton.clicked.connect(self.goto_dir_parent)
        pathBarLayout.addWidget(self.parentButton)
        self.refreshButton = QPushButton('Go!')
        self.refreshButton.setIcon(self.style().standardIcon(QStyle.SP_DirOpenIcon))
        self.refreshButton.clicked.connect(self.update_treeview)
        pathBarLayout.addWidget(self.refreshButton)
        layout.addLayout(pathBarLayout)

        self.treeview = BrowserTreeView()
        self.treeview.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.treeview.doubleClicked.connect(self.treeview_double_clicked)
        self.treeview.contextMenuEvent = self.treeviewContextMenuEvent
        layout.addWidget(self.treeview)

        self.treemodel = QStandardItemModel(0, 4, self)
        self.treeview.setModel(self.treemodel)
        self.treemodel.setHeaderData(self.FILE_NAME, Qt.Horizontal, "Name")
        self.treemodel.setHeaderData(self.FILE_TYPE, Qt.Horizontal, "Type")
        self.treemodel.setHeaderData(self.FILE_SIZE, Qt.Horizontal, "Size")
        self.treemodel.setHeaderData(self.FILE_MIMETYPE, Qt.Horizontal, "Mime Type")
        #endregion

        #region Context Menu
        #region File
        self.fileContextMenu = QMenu('File Context')
        #region
        openAction = QAction("Open", self)
        openAction.setStatusTip('Open the selected file')
        openAction.triggered.connect(self.open_selected_file)
        self.fileContextMenu.addAction(openAction)
        #endregion
        self.fileContextMenu.addSeparator()
        #region
        copyAction = QAction("Copy", self)
        copyAction.setStatusTip('Copy the selected file')
        copyAction.triggered.connect(self.copy_path)
        self.fileContextMenu.addAction(copyAction)
        #endregion
        #region
        cutAction = QAction("Cut", self)
        cutAction.setStatusTip('Cut the selected file')
        cutAction.triggered.connect(self.cut_path)
        self.fileContextMenu.addAction(cutAction)
        #endregion
        self.fileContextMenu.addSeparator()
        #region
        renameAction = QAction("Rename", self)
        renameAction.setStatusTip('Rename selected file')
        renameAction.triggered.connect(self.rename_selected_file)
        self.fileContextMenu.addAction(renameAction)
        #endregion
        #region
        trashAction = QAction("Move to " + ('recycle bin' if os.name == 'nt' else 'trash'), self)
        trashAction.setStatusTip('Move the selected file to ' + ('the recycle bin' if os.name == 'nt' else 'trash'))
        trashAction.triggered.connect(self.trash_selected_file)
        self.fileContextMenu.addAction(trashAction)
        #endregion
        #endregion

        #region Folder
        self.folderContextMenu = QMenu('Folder Context')
        #region
        pasteAction = QAction("Paste", self)
        pasteAction.setStatusTip('Paste the file whose path is in the clipboard')
        pasteAction.triggered.connect(self.paste_path)
        self.folderContextMenu.addAction(pasteAction)
        #endregion
        self.folderContextMenu.addSeparator()
        #region
        createFileAction = QAction("New file", self)
        createFileAction.setStatusTip('Create a file in this folder')
        createFileAction.triggered.connect(self.create_file)
        self.folderContextMenu.addAction(createFileAction)
        #endregion
        #region
        createFolderAction = QAction("New Folder", self)
        createFolderAction.setStatusTip('Folder a folder in this folder') # That is definitely incorrect, and I will fix it at a later date
        createFolderAction.triggered.connect(self.create_folder)
        self.folderContextMenu.addAction(createFolderAction)
        #endregion
        #endregion
        #endregion

        self.config = {
            'typing': {
                'types': {
                    'text': {
                        'viewer': 'gedit "%s"',
                        'mimetypes': [
                            'text/plain',
                            'text/x-python'
                        ]
                    },
                    'binary': {
                        'viewer': 'ghex "%s"',
                        'mimetypes': [
                            'application/binary'
                        ]
                    }
                },
                'mimetypes': {
                    'text/plain': {
                        'name': 'Plain Text File',
                        'patterns': [
                            r'.*\.gitignore$'
                        ]
                    },
                    'application/binary': {
                        'name': 'Binary File',
                        'patterns': []
                    },
                    'text/x-python': {
                        'name': 'Python Script',
                        'patterns': []
                    }
                }
            }
        }

        self.hide_files = True

        self.set_current_path('.')
        self.update_treeview()

    def correct_path(self, path: os.PathLike) -> pathlib.Path:
        return pathlib.Path(path).expanduser().absolute().resolve()

    def get_current_path(self) -> pathlib.Path:
        return self.correct_path(self.pathedit.text())

    def set_current_path(self, path: os.PathLike) -> bool:
        path = self.correct_path(path)
        if not self.does_path_exist(path):
            return False
        if not self.is_path_dir(path):
            return False
        self.pathedit.setText(str(path))
        return True

    def determine_if_file_hidden(self, path: os.PathLike) -> bool:
        return pathlib.Path(path).name.startswith('.')

    def list_directory(self, path: os.PathLike):
        return os.listdir(path)

    def get_file_size(self, path: os.PathLike):
        return os.stat(path).st_size

    def is_path_file(self, path: os.PathLike):
        return os.path.isfile(path)

    def is_path_dir(self, path: os.PathLike):
        return os.path.isdir(path)

    def does_path_exist(self, path: os.PathLike):
        return os.path.exists(path)

    def clipboard_copy(self, text: str):
        clipboard = QApplication.clipboard()
        mimedata = QMimeData()
        mimedata.setText(text)
        clipboard.setMimeData(mimedata)

    def clipboard_paste(self) -> str:
        clipboard = QApplication.clipboard()
        mimeData = clipboard.mimeData()
        if mimeData.hasText():
            return mimeData.text()
        else:
            show_critical_messagebox('Clipboard Error', 'The data stored in the clipboard isn\'t plain text\nSimplified terms: You copied something that isn\'t text')
            return ''

    def update_treeview(self):
        self.updating_treevew = True
        path = self.get_current_path()
        if self.does_path_exist(path):
            if self.is_path_dir(path):
                try:
                    dir_listing = self.list_directory(path)
                    dir_listing.sort(reverse=True)
                    self.treemodel.removeRows(0, self.treemodel.rowCount())
                    for item in dir_listing:
                        if self.hide_files and self.determine_if_file_hidden(pathlib.Path(path, item)):
                            continue
                        item_path = pathlib.Path(path, item)
                        item_size = self.get_file_size(item_path)
                        self.treemodel.insertRow(0)
                        self.treemodel.setData(self.treemodel.index(0, self.FILE_NAME), item)
                        self.treemodel.setData(self.treemodel.index(0, self.FILE_TYPE), self.get_type_name(item_path))
                        self.treemodel.setData(self.treemodel.index(0, self.FILE_SIZE), convert_size(item_size) if self.is_path_file(item_path) else 'N/A')
                        self.treemodel.setData(self.treemodel.index(0, self.FILE_MIMETYPE), self.get_mimetype(item_path) if self.is_path_file(item_path) else 'N/A')
                        self.treemodel.setItemData(self.treemodel.index(0, self.FILE_NAME), {Qt.DecorationRole: self.style().standardIcon(QStyle.SP_FileIcon if self.is_path_file(item_path) else QStyle.SP_DirIcon if self.is_path_dir(item_path) else QStyle.SP_DialogNoButton)})
                except PermissionError as e:
                    show_critical_messagebox('Access denied', 'You don\'t have permission to access that.\n%s: %s' % (PermissionError.__name__, str(e)))
            else:
                show_critical_messagebox('Not a directory', 'The path you entered isn\'t a directory')
        else:
            show_critical_messagebox('No such file or directory', 'The path you entered doesn\'t exist')

        self.treeview.resizeColumnToContents(self.FILE_NAME)
        self.treeview.resizeColumnToContents(self.FILE_SIZE)
        self.treeview.resizeColumnToContents(self.FILE_TYPE)
        self.treeview.resizeColumnToContents(self.FILE_MIMETYPE)

        self.updating_treevew = False
        self.directory_listing_refreshed.emit()

    def treeview_double_clicked(self, index):
        file = index.model().itemFromIndex(self.treemodel.index(index.row(), self.FILE_NAME)).text()
        path = pathlib.Path(self.get_current_path(), file)
        self.open_file(path)

    def get_selected_path(self):
        index = self.treeview.currentIndex()
        file = index.model().itemFromIndex(self.treemodel.index(index.row(), self.FILE_NAME)).text()
        path = pathlib.Path(self.get_current_path(), file)
        return path

    def open_selected_file(self):
        path = self.get_selected_path()
        self.open_file(path)

    def trash_selected_file(self):
        path = self.get_selected_path()
        self.trash_file(path)

    def open_file(self, path):
        if self.is_path_dir(path):
            self.set_current_path(path)
            self.update_treeview()
        elif self.is_path_file(path):
            if not self.view_file(path):
                show_critical_messagebox('Error', 'Failed to open file. Check your configuration')

    def trash_file(self, path):
        try:
            trashfile(path)
            self.update_treeview()
        except PermissionError as e:
            show_critical_messagebox('Error', 'You are not permitted to move that file\n%s: %s' % (e.__class__.__name__, str(e)))

    def rename_selected_file(self):
        path = self.get_selected_path()
        old_path = pathlib.Path(path)
        try:
            if not (self.is_path_file(old_path) or self.is_path_dir(old_path)):
                show_critical_messagebox('Error', 'That\'s not a file or directory')
                return

            index = self.treeview.currentIndex()
            index = self.treemodel.index(index.row(), self.FILE_NAME)

            self.treeview.edit(index)

            loop = QEventLoop()
            self.treeview.editor_closed.connect(loop.quit)
            loop.exec() # wait ...

            text = self.get_selected_path().name

            if not text:
                return
            entered_path = pathlib.Path(text)
            new_path = pathlib.Path(old_path.parent, entered_path.name)
            if new_path == old_path:
                return
            if self.does_path_exist(new_path):
                show_critical_messagebox('Error', 'A file with that name already exists')
                self.rename_selected_file()
                return
            shutil.move(old_path, new_path)
            self.update_treeview()
        except PermissionError as e:
            show_critical_messagebox('Error', 'You are not permitted to move that file\n%s: %s' % (e.__class__.__name__, str(e)))

    def treeviewContextMenuEvent(self, event):
        point = self.treeview.mapFromGlobal(QCursor.pos())
        vmargin = self.treeview.header().height()
        point.setY(point.y() - vmargin)
        row = self.treeview.indexAt(point).row()
        if row >= 0:
            self.fileContextMenu.popup(QCursor.pos())
        else:
            self.folderContextMenu.popup(QCursor.pos())

    def goto_dir_parent(self):
        self.set_current_path(self.get_current_path().parent)
        self.update_treeview()

    def get_mimetype(self, path):
        for mimetype in self.config['typing']['mimetypes']:
            for pattern in self.config['typing']['mimetypes'][mimetype]['patterns']:
                if re.findall(pattern, str(path)):
                    return mimetype
        mimetype = mimetypes.MimeTypes().guess_type(path)[0]
        return mimetype if mimetype else 'application/octet-stream'

    def view_file(self, path, mimetype=None) -> bool:
        if not mimetype:
            mimetype = self.get_mimetype(path)
        if not mimetype in self.config['typing']['mimetypes']:
            return False
        assoc_file_type = None
        for file_type in self.config['typing']['types']:
            if mimetype in self.config['typing']['types'][file_type]['mimetypes']:
                assoc_file_type = file_type
                break
        if not assoc_file_type:
            return False
        if not self.config['typing']['types'][file_type]['viewer']:
            return False
        command = self.config['typing']['types'][file_type]['viewer'] % str(path).replace('"', '\\"')
        process = subprocess.Popen(shlex.split(command))
        return True

    def get_type_name(self, path):
        return_data = 'File' if self.is_path_file(path) else 'Folder' if self.is_path_dir(path) else 'Unknown'
        if return_data == 'File':
            mimetype = self.get_mimetype(path)
            if mimetype in self.config['typing']['mimetypes']:
                return_data = self.config['typing']['mimetypes'][mimetype]['name']
        return return_data

    def create_file(self):
        text = ask_text('New File', 'Use the following field to enter the name of the new file', 'Enter file name here')
        if text:
            entered_path = pathlib.Path(text)
            path = pathlib.Path(self.get_current_path(), entered_path.name)
            if self.does_path_exist(path):
                show_critical_messagebox('Error', 'A file or folder with that name already exists')
                return
            with open(path, mode='w') as f: f.write('')
            self.update_treeview()

    def create_folder(self):
        text = ask_text('New Folder', 'Use the following field to enter the name of the new folder', 'Enter folder name here')
        if text:
            entered_path = pathlib.Path(text)
            path = pathlib.Path(self.get_current_path(), entered_path.name)
            if self.does_path_exist(path):
                show_critical_messagebox('Error', 'A file or folder with that name already exists')
                return
            os.mkdir(path)
            self.update_treeview()

    def edit_file_name(self):
        row = self.treeview.currentIndex().row()
        index = self.treemodel.index(row, self.FILE_NAME)
        cur_item = self.treemodel.itemFromIndex(index)
        cur_text = cur_item.text()
        self.treeview.edit(index)

    def update_hide_files(self, data):
        self.hide_files = data
        self.update_treeview()

    def pathedit_text_updated(self):
        new_path = self.pathedit.text()
        self.set_current_path(new_path)
        self.update_treeview()

    def paste_path(self):
        text = self.clipboard_paste()
        if not text:
            return
        if not len(text.split('\n')) == 2:
            show_critical_messagebox('Error', 'Invalid clipboard contents')
            return
        command, path_str = text.split('\n')
        path = self.correct_path(path_str)
        dst_path = pathlib.Path(self.get_current_path(), path.name)
        if not path.exists():
            show_critical_messagebox('File not found error', 'The path stored in your clipboard doesn\'t exist. The path you in your clipboard:\n%s' % path_str)
            self.update_treeview()
            return
        match command:
            case 'copy':
                shutil.copy(path, dst_path)
            case 'cut':
                shutil.move(path, dst_path)
            case _:
                show_critical_messagebox('Error', 'Unknown paste command:\n%s' % command)
        self.update_treeview()

    def copy_path(self):
        path = self.get_selected_path()
        output = 'copy\n%s'
        self.clipboard_copy(output % path)

    def cut_path(self):
        path = self.get_selected_path()
        output = 'cut\n%s'
        self.clipboard_copy(output % path)
